﻿/*
 *  RacerLib - XNA Racing Game Level and Tile Management Library
 * 
 *  Copyright (C) 2009 Sebastian Schäfer
 *  
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; version 2 of the License.
 *  
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License 
 *  along with this program; if not, see <http://www.gnu.org/licenses/>.
 */
using System;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Linq;

using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Xml.Serialization;

namespace gdv.VCPR.Racer
{
    /// <summary>
    /// Base class for trackinformations
    /// </summary>
    [Serializable()]
    public class TrackInfo : ISerializable
    {
        /// <summary>
        /// Who created this track
        /// </summary>
        public String Creator { get; set; }
        /// <summary>
        /// Name
        /// </summary>
        public String Trackname { get; set; }
        /// <summary>
        /// Version
        /// </summary>
        public String Trackversion { get; set; }

        // <summary>
        // Preview image
        // </summary>
        //public Bitmap Preview { get; set; }

        /// <summary>
        /// Contruct an instance using default values
        /// </summary>
        public TrackInfo():
            this("none", "JustCreated", "0.23")
        {
        }

        /// <summary>
        /// Construct an instance
        /// </summary>
        /// <param name="creator">Name of creator</param>
        /// <param name="trackname">Name of track</param>
        /// <param name="trackversion">Version of track</param>
        public TrackInfo(String creator, String trackname, String trackversion)
        {
            Creator = creator;
            Trackname = trackname;
            Trackversion = trackversion;
        }

        /// <summary>
        /// Serialization constructor
        /// </summary>
        /// <param name="info">SerializationInfo</param>
        /// <param name="ctxt">StreamingContext</param>
        public TrackInfo(SerializationInfo info, StreamingContext ctxt)
        {
            Creator = (String)info.GetValue("Creator", typeof(String));
            Trackname = (String)info.GetValue("Trackname", typeof(String));
            Trackversion = (String)info.GetValue("Trackversion", typeof(String));
            try
            {
                //Preview = (Bitmap)info.GetValue("Preview", typeof(Bitmap));
            }
            catch (Exception)
            {
                //Preview = new Bitmap(128, 128);
            }
        }

        /// <summary>
        /// Set all values using a single command
        /// </summary>
        /// <param name="creator">Name of creator</param>
        /// <param name="trackname">Name of track</param>
        /// <param name="trackversion">Version of track</param>
        public void Set(String creator, String trackname, String trackversion)
        {
            Creator = creator;
            Trackname = trackname;
            Trackversion = trackversion;
        }

        /// <summary>
        /// Create a flat copy
        /// </summary>
        /// <returns>new instance, same data</returns>
        public TrackInfo Clone()
        {
            return new TrackInfo(Creator, Trackname, Trackversion);
        }

        #region Object Methods
        public override bool Equals(object obj)
        {
            if (obj.GetType() == typeof(TrackInfo))
            {
                TrackInfo ti = obj as TrackInfo;
                return !(ti.Creator != Creator
                        || ti.Trackname != Trackname
                               || ti.Trackversion != Trackversion);
            }
            else
                return false;
        }

        /// <summary>
        /// Creates the hash code for this track info object.
        /// </summary>
        /// <returns>hash code</returns>
        public override int GetHashCode()
        {
            int hashCode = 17;
            hashCode = hashCode * 31 + Creator.GetHashCode();
            hashCode = hashCode * 31 + Trackname.GetHashCode();
            hashCode = hashCode * 31 + Trackversion.GetHashCode();
            return hashCode;
        }
        #endregion

        #region ISerializable Member

        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("Creator", Creator);
            info.AddValue("Trackname", Trackname);
            info.AddValue("Trackversion", Trackversion);
            //info.GetValue("Preview", Preview);
        }

        #endregion
    }

    /// <summary>
    /// Class that contains a track
    /// </summary>
    [Serializable()]
    public class Track : ISerializable
    {
        /// <summary>
        /// Number of tiles along X-axis
        /// </summary>
        public int Width { get; set; }
        /// <summary>
        /// Number of tiles along Y-axis
        /// </summary>
        public int Height { get; set; }

        /// <summary>
        /// Version of trackfile (= different supported features)
        /// </summary>
        public String TrackfileVersion { get; set; }

        /// <summary>
        /// Trackinformation object
        /// </summary>
        public TrackInfo TrackInfo { get; set; }

        /// <summary>
        /// All tiles that form the track
        /// </summary>
        public Collection<Tile> Tiles { get; set; }

        /// <summary>
        /// The start tile of current track. if none present returns null
        /// </summary>
        public Tile StartTile
        {
            get
            {
                foreach (Tile tile in Tiles)
                    if (tile.HasFlag(ETileTypeFlags.Start))
                        return tile;
                return null;
            }
        }

        /// <summary>
        /// The goal tile of current track. if none present returns null
        /// </summary>
        public Tile GoalTile
        {
            get
            {

                foreach (Tile tile in Tiles)
                    if (tile.HasFlag(ETileTypeFlags.Goal))
                        return tile;
                return null;
            }
        }

        /// <summary>
        /// The checkpoint tiles of current track. if none present returns empty array
        /// </summary>
        public Tile[] CheckpointTiles
        {
            get
            {
                Collection<Tile> res = new Collection<Tile>();
                foreach (Tile tile in Tiles)
                    if (tile.HasFlag(ETileTypeFlags.Checkpoint))
                        res.Add(tile);
                return res.ToArray<Tile>();
            }
        }

        /// <summary>
        /// Create an empty Track using default values
        /// </summary>
        public Track()
            :this(32, 32)
        {
        }

        /// <summary>
        /// Create an empty Track using default values
        /// </summary>
        ///  <param name="width">Width</param>
        ///  <param name="height">Height</param>
        public Track(int width, int height)
            :this(width, height, "0.1", new TrackInfo(), new Collection<Tile>())
        {
        }

        /// <summary>
        /// Create a Track with given values
        /// </summary>
        /// <param name="width">Width</param>
        /// <param name="height">Height</param>
        /// <param name="trackfileVersion">TrackfileVersion</param>
        /// <param name="trackInfo">TrackfileInfo</param>
        /// <param name="tiles">Tiles</param>
        public Track(int width, int height, String trackfileVersion, TrackInfo trackInfo, Collection<Tile> tiles)
        {
            Width = width;
            Height = height;
            TrackfileVersion = trackfileVersion;
            TrackInfo = trackInfo;
            Tiles = tiles;
        }

        /// <summary>
        /// Serialization constructor
        /// </summary>
        /// <param name="info">SerializationInfo</param>
        /// <param name="ctxt">StreamingContext</param>
        public Track(SerializationInfo info, StreamingContext ctxt)
        {
            Width = (int)info.GetValue("Width", typeof(int));
            Height = (int)info.GetValue("Height", typeof(int));
            TrackfileVersion = (String)info.GetValue("TrackfileVersion", typeof(String));
            
            TrackInfo= (TrackInfo)info.GetValue("TrackInfo", typeof(TrackInfo));
            Tiles = (Collection<Tile>)info.GetValue("Tiles", typeof(Collection<Tile>));
        }

        /// <summary>
        /// Create a flat copy
        /// </summary>
        /// <returns>new instance, same data</returns>
        public Track Clone()
        {
            Collection<Tile> tiles = new Collection<Tile>();
            foreach (Tile tile in Tiles)
                tiles.Add(tile.Clone());
            return new Track(Width, Height, TrackfileVersion, TrackInfo.Clone(), tiles);
        }

        /// <summary>
        /// create a human readable string 
        /// </summary>
        /// <returns>returns the string</returns>
        public override string ToString()
        {
            return TrackInfo.Trackname + "(v. " + TrackInfo.Trackversion + "): " + Width + "x" + Height + " ("+ Tiles.Count+ "tiles) by " + TrackInfo.Creator;
        }


        #region TileManagement
        /// <summary>
        /// Remove tiles that are "out of field"
        /// </summary>
        public void CleanupTiles()
        {
            foreach (Tile tile in Tiles)
                if ((tile.X > Width) || (tile.Y > Height))
                    Tiles.Remove(tile);
        }

        /// <summary>
        /// Checks if a tile exists at given Location
        /// </summary>
        /// <param name="x">X-position</param>
        /// <param name="y">Y-position</param>
        /// <returns>the index within the collection</returns>
        public int ContainsTileAt(int x, int y)
        {
            foreach (Tile tile in Tiles)
                if ((tile.X == x) && (tile.Y == y))
                    return Tiles.IndexOf(tile);
            return -1;
        }

        /// <summary>
        /// Checks if a tile exists at the same Location - only X and Y coordinates are used!
        /// </summary>
        /// <param name="t">The tile that is at the position</param>
        /// <returns>the index within the collection</returns>
        public int ContainsTileAt(Tile t)
        {
            foreach (Tile tile in Tiles)
                if (t.SamePosition(tile))
                    return Tiles.IndexOf(tile);
            return -1;
        }

        /// <summary>
        /// Adds a new tile to the Tiles-collection
        /// </summary>
        /// <param name="tile">the new tile</param>
        /// <param name="replace">wether or not a tile at the same position should be replaced</param>
        public void AddTile(Tile tile, bool replace)
        {
            int pos = ContainsTileAt(tile);
            if (pos >= 0)
            {
                if (!replace)
                    return;
                else
                    Tiles.RemoveAt(pos);
            }
            Tiles.Add(tile);
        }

        /// <summary>
        /// Adds a new tile to the Tiles-collection, any existing tiles are beeing replaced
        /// </summary>
        /// <param name="tile">the new tile</param>
        public void AddTile(Tile tile)
        {
            AddTile(tile, true);
        }

        /// <summary>
        /// Remove a tile at the given position
        /// </summary>
        /// <param name="tile">The tile that is at the position</param>
        public void RemoveTileAt(Tile tile)
        {
            int pos = ContainsTileAt(tile);
            if (pos >= 0)
                Tiles.RemoveAt(pos);
        }

        /// <summary>
        /// Remove a tile at the given position
        /// </summary>
        /// <param name="x">X-position</param>
        /// <param name="y">Y-position</param>
        public void RemoveTileAt(int x, int y)
        {
            int pos = ContainsTileAt(x, y);
            if (pos >= 0)
                Tiles.RemoveAt(pos);
        }

        /// <summary>
        /// Set a new size and perform a cleanup
        /// </summary>
        /// <param name="width">new width</param>
        /// <param name="height">new height</param>
        public void SetSize(int width, int height)
        {
            Width = width;
            Height = height;
            CleanupTiles();
        }
        #endregion

        #region Load/Save
        /// <summary>
        /// load a track Version 0.1
        /// </summary>
        /// <param name="filename">Absolute or relative path</param>
        /// <returns>new track-object, null if it was not loadable</returns>
        private static Track load_v01(String filename)
        {
            Stream stream = new FileStream(filename, FileMode.Open);
            BinaryFormatter bf = new BinaryFormatter();

            Track track = null;
            try
            {
                track = (Track)bf.Deserialize(stream);
            }
            catch (Exception)
            {
                //Wrong file version
            }
            stream.Close();
            return track;
        }

        /// <summary>
        /// load XML
        /// </summary>
        /// <param name="filename">Absolute or relative path</param>
        /// <returns>new track-object, null if it was not loadable</returns>
        private static Track load_xml(String filename)
        {
            Stream stream = new FileStream(filename, FileMode.Open);

            Track track = null;
            try
            {
                XmlSerializer xml = new XmlSerializer(typeof(Track));
                track = (Track)xml.Deserialize(stream);
            }
            catch (Exception e)
            {
                //Wrong file version
                System.Console.Write(e.Message);
            }
            stream.Close();
            return track;
        }

        /// <summary>
        /// save a track Version 0.1
        /// </summary>
        /// <param name="filename">Absolute or relative path</param>
        /// <param name="track">the track to be saved</param>
        private static void save_v01(String filename, Track track)
        {
            Stream stream = new FileStream(filename, FileMode.Create);
            BinaryFormatter bf = new BinaryFormatter();
            bf.Serialize(stream, track);
            stream.Close();
        }

        /// <summary>
        /// Load a track. Detect and use approrpiate Loading-routine
        /// </summary>
        /// <param name="filename">Absolute or relative path</param>
        /// <returns>new track-object, null if it was not loadable</returns>
        public static Track Load(String filename)
        {
            Track track = null;

            track = load_xml(filename);
            if (track == null)
                track = load_v01(filename);
            
            return track;
        }

        /// <summary>
        /// Save a track using a binary serialization
        /// </summary>
        /// <param name="filename">Absolute or relative path</param>
        /// <param name="track">the track to be saved</param>
        public static void SaveBinary(String filename, Track track)
        {
            save_v01(filename, track);
        }

        /// <summary>
        /// Save a track using the XML-serializer
        /// </summary>
        /// <param name="filename">Absolute or relative path</param>
        /// <param name="track">the track to be saved</param>
        public static void Save(String filename, Track track)
        {
            Stream stream = new FileStream(filename, FileMode.Create);
            XmlSerializer xml = new XmlSerializer(track.GetType());
            //bf.Serialize(stream, track);
            xml.Serialize(stream, track);
            stream.Close();
        }
        #endregion

        #region ISerializable Member
        /// <summary>
        /// Serialize Deconstructor
        /// </summary>
        /// <param name="info">SerializationInfo</param>
        /// <param name="context">StreamingContext</param>
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("Width", Width);
            info.AddValue("Height", Height);
            info.AddValue("TrackfileVersion", TrackfileVersion);

            info.AddValue("TrackInfo", TrackInfo);
            info.AddValue("Tiles", Tiles);
        }

        #endregion
    }
}
